﻿//******************************************************************************************************
//  Generator.cs - Gbtc
//
//  Copyright © 2010, Grid Protection Alliance.  All Rights Reserved.
//
//  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
//  the NOTICE file distributed with this work for additional information regarding copyright ownership.
//  The GPA licenses this file to you under the Eclipse Public License -v 1.0 (the "License"); you may
//  not use this file except in compliance with the License. You may obtain a copy of the License at:
//
//      http://www.opensource.org/licenses/eclipse-1.0.php
//
//  Unless agreed to in writing, the subject software distributed under the License is distributed on an
//  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
//  License for the specific language governing permissions and limitations.
//
//  Code Modification History:
//  ----------------------------------------------------------------------------------------------------
//  07/07/2010 - Stephen C. Wills
//       Generated original version of source code.
//
//******************************************************************************************************

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using TVA;
using TVA.Communication;
using TVA.PhasorProtocols;
using TVA.PhasorProtocols.IeeeC37_118;

namespace TestBenchApi
{
    /// <summary>
    /// Represents a generator used to send test data to the concentrator.
    /// </summary>
    public class Generator
    {

        #region [ Members ]

        // Nested Types

        private class HighResolutionParser
        {

            #region [ Members ]

            // Nested Types

            // Constants

            // Delegates

            // Events

            // Fields

            private MultiProtocolFrameParser m_parser;
            private Dictionary<long, long> m_unexpectedTimestamps;
            private long m_frameCount;
            private int m_frameRate;
            private int m_expectedFrameIndex;

            #endregion

            #region [ Constructors ]

            /// <summary>
            /// Creates a new instance of the <see cref="HighResolutionParser"/> class.
            /// </summary>
            /// <param name="frameRate">The frame rate of the parser in frames per second.</param>
            public HighResolutionParser(int frameRate)
            {
                m_parser = new MultiProtocolFrameParser();
                m_unexpectedTimestamps = new Dictionary<long, long>();
                m_frameRate = frameRate;
                m_frameCount = 0;
                m_expectedFrameIndex = -1;

                m_parser.AutoRepeatCapturedPlayback = true;
                m_parser.AutoStartDataParsingSequence = true;
                m_parser.DefinedFrameRate = frameRate;
                m_parser.InjectSimulatedTimestamp = true;
                m_parser.UseHighResolutionInputTimer = true;
                m_parser.ReceivedDataFrame += m_parser_ReceivedDataFrame;
            }

            #endregion

            #region [ Properties ]

            /// <summary>
            /// The high resolution frame parser.
            /// </summary>
            public MultiProtocolFrameParser FrameParser
            {
                get
                {
                    return m_parser;
                }
            }

            /// <summary>
            /// The frame count mapped to the unexpected timestamps.
            /// </summary>
            public Dictionary<long, long> UnexpectedTimestamps
            {
                get
                {
                    return m_unexpectedTimestamps;
                }
            }

            #endregion

            #region [ Methods ]

            // Occurs when the parser receives a data frame.
            private void m_parser_ReceivedDataFrame(object sender, EventArgs<IDataFrame> e)
            {
                long timestampBeyondSecond = e.Argument.Timestamp.DistanceBeyondSecond();
                long expectedMillis = (m_expectedFrameIndex * 1000 / m_frameRate);
                long timestampMillis = timestampBeyondSecond / Ticks.PerMillisecond;

                m_frameCount++;
                m_expectedFrameIndex = (m_expectedFrameIndex + 1) % m_frameRate;

                if (expectedMillis != timestampMillis)
                {
                    m_expectedFrameIndex = (int)Math.Ceiling((double)timestampBeyondSecond * m_frameRate / Ticks.PerSecond);
                    m_unexpectedTimestamps.Add(m_frameCount, e.Argument.Timestamp);
                }
            }

            #endregion
        }

        // Fields

        private string m_inputFile;
        private string m_configurationFrameFile;
        private int m_frameRate;
        private TransportProtocol m_outputProtocol;
        private ushort m_accessId;
        private string m_concentratorHostName;
        private int m_port;

        private HighResolutionParser m_highResolutionParser;
        private MultiProtocolFrameParser m_parser;
        private IConfigurationFrame m_configurationFrame;
        private TcpServer m_commandChannel;
        private UdpClient m_dataChannel;
        private byte[] m_commandChannelBuffer;

        #endregion

        #region [ Constructors ]

        /// <summary>
        /// Creates a new instance of the <see cref="Generator"/> class.
        /// </summary>
        public Generator()
        {
        }

        #endregion

        #region [ Methods ]

        /// <summary>
        /// Initializes the generator.
        /// </summary>
        /// <param name="settings">The settings needed to initialize the generator.</param>
        public void Initialize(Dictionary<string, string> settings)
        {
            // Load settings.
            m_inputFile = settings["inputFile"];
            m_configurationFrameFile = settings["configFrameFile"];
            m_frameRate = int.Parse(settings["frameRate"]);
            m_outputProtocol = (TransportProtocol)Enum.Parse(typeof(TransportProtocol), settings["outputProtocol"]);
            m_accessId = ushort.Parse(settings["accessId"]);
            m_concentratorHostName = settings["concentratorHostName"];
            m_port = int.Parse(settings["port"]);

            // Initialize components.
            InitializeHighResolutionParser();
            InitializeCommandChannel();
            InitializeDataChannel();
        }

        /// <summary>
        /// Starts the generator.
        /// </summary>
        public void Start()
        {
            m_commandChannel.Start();
        }

        /// <summary>
        /// Stops the generator.
        /// </summary>
        public void Stop()
        {
            lock (m_parser)
            {
                if (m_parser.Enabled)
                    m_parser.Stop();
            }

            m_commandChannel.Stop();
        }

        // Initializes the high resolution parser.
        private void InitializeHighResolutionParser()
        {
            lock (s_highResolutionParsers)
            {
                if (!s_highResolutionParsers.TryGetValue(m_inputFile, out m_highResolutionParser))
                {
                    m_highResolutionParser = new HighResolutionParser(m_frameRate);
                    m_highResolutionParser.FrameParser.ConnectionString = string.Format("phasorProtocol=IeeeC37_118V1; accessId={0}; transportProtocol=File; file={1}", m_accessId, m_inputFile);
                    s_highResolutionParsers.Add(m_inputFile, m_highResolutionParser);
                }

                m_configurationFrame = TVA.PhasorProtocols.Common.DeserializeConfigurationFrame(m_configurationFrameFile);
                m_parser = m_highResolutionParser.FrameParser;
                m_parser.ReceivedDataFrame += m_frameParser_ReceivedDataFrame;
            }
        }

        // Initializes the command channel.
        private void InitializeCommandChannel()
        {
            string configString = string.Format("port={0}", m_port);
            m_commandChannel = new TcpServer(configString);
            m_commandChannel.ReceiveClientDataComplete += m_commandChannel_ReceiveClientDataComplete;
        }

        // Initializes the data channel.
        private void InitializeDataChannel()
        {
            if (m_outputProtocol == TransportProtocol.Udp)
            {
                string configString = string.Format("server={0}:{1}; port={1}", m_concentratorHostName, m_port);
                m_dataChannel = new UdpClient(configString);
            }
        }

        private long m_time;

        // Receives the first parsed data frame from the frame parser.
        private void m_frameParser_GetFirstDataFrameTimestamp(object sender, EventArgs<IDataFrame> e)
        {
            Interlocked.CompareExchange(ref s_firstDataFrameTimestamp, e.Argument.Timestamp, -1L);
            m_time = PrecisionTimer.UtcNow.Ticks;
            m_parser.ReceivedDataFrame -= m_frameParser_GetFirstDataFrameTimestamp;
        }

        // Sends data frames to the concentrator.
        private void m_frameParser_ReceivedDataFrame(object sender, EventArgs<IDataFrame> e)
        {
            byte[] data = e.Argument.BinaryImage;
            int len = e.Argument.BinaryLength;

            if (m_dataChannel != null)
                m_dataChannel.Send(data, 0, len);
            else
            {
                foreach (Guid clientId in m_commandChannel.ClientIDs)
                    m_commandChannel.SendTo(clientId, data, 0, len);
            }
        }

        // Receives data from the command channel.
        private void m_commandChannel_ReceiveClientDataComplete(object sender, TVA.EventArgs<Guid, byte[], int> e)
        {
            byte[] data = e.Argument2;
            int offset = 0;
            int length = e.Argument3;

            if (m_commandChannelBuffer != null && m_commandChannelBuffer.Length > 0)
            {
                data = m_commandChannelBuffer.Combine(0, m_commandChannelBuffer.Length, data, 0, length);
                length = data.Length;
            }

            while (length >= CommonFrameHeader.FixedLength)
            {
                CommonFrameHeader header = new CommonFrameHeader(null, data, 0);
                int frameLength = header.FrameLength;

                if (length < frameLength)
                    break;
                else
                {
                    DeviceCommandHandler(e.Argument1, data, offset, length);
                    offset += frameLength;
                    length -= frameLength;
                }
            }

            m_commandChannelBuffer = data.BlockCopy(offset, length);
        }
        
        // Handles incoming commands from devices connected over the command channel.
        private void DeviceCommandHandler(Guid clientID, byte[] commandBuffer, int offset, int length)
        {
            CommandFrame commandFrame = new CommandFrame(commandBuffer, 0, length);

            switch (commandFrame.Command)
            {
                case DeviceCommand.SendConfigurationFrame1:
                case DeviceCommand.SendConfigurationFrame2:
                    lock (m_parser)
                    {
                        foreach (Guid clientId in m_commandChannel.ClientIDs)
                            m_commandChannel.SendTo(clientId, m_configurationFrame.BinaryImage);

                        if (!m_parser.Enabled)
                        {
                            m_parser.Start();
                            m_parser.ConfigurationFrame = m_configurationFrame;
                        }
                    }

                    break;
            }
        }

        #endregion

        #region [ Static ]

        // Static Fields

        private static Dictionary<string, HighResolutionParser> s_highResolutionParsers;
        private static long s_firstDataFrameTimestamp;

        // Static Constructor

        static Generator()
        {
            s_highResolutionParsers = new Dictionary<string, HighResolutionParser>();
            s_firstDataFrameTimestamp = -1L;
        }

        // Static Methods

        /// <summary>
        /// Prepends the unexpected timestamps to the given timestamp file.
        /// This adds an extra 64-bit integer indicating the position at which the original contents reside.
        /// </summary>
        /// <param name="timestampFilePath"></param>
        public static void PrependUnexpectedTimestamps(string timestampFilePath)
        {
            // Assume there is only one parser in the dictionary.
            HighResolutionParser parser = s_highResolutionParsers.Values.First();
            Dictionary<long, long> unexpectedTimestamps = parser.UnexpectedTimestamps;

            string tempFilePath = Path.GetTempFileName();
            BinaryWriter tempFileWriter = null;
            BinaryReader timestampFileReader = null;
            byte[] buffer = new byte[32768];
            int bytesRead;

            try
            {
                // Write the position at which the original contents will reside.
                tempFileWriter = new BinaryWriter(File.Create(tempFilePath));
                tempFileWriter.Write(8 + unexpectedTimestamps.Count * 16L);

                // Write the frame counts followed by the corresponding unexpected timestamp.
                foreach (long frameCount in unexpectedTimestamps.Keys)
                {
                    tempFileWriter.Write(frameCount);
                    tempFileWriter.Write(unexpectedTimestamps[frameCount]);
                }

                // Open the timestamp file.
                timestampFileReader = new BinaryReader(File.OpenRead(timestampFilePath));

                // Write the contents of the timestamp file to the temp file.
                do
                {
                    bytesRead = timestampFileReader.Read(buffer, 0, buffer.Length);
                    tempFileWriter.Write(buffer, 0, bytesRead);
                } while (bytesRead > 0);
            }
            finally
            {
                if (tempFileWriter != null)
                    tempFileWriter.Close();

                if (timestampFileReader != null)
                    timestampFileReader.Close();
            }

            File.Replace(tempFilePath, timestampFilePath, null);
        }

        #endregion
        
    }
}
